/*
 * The Smacker library was ported from C# for use in Silvie. The original
 * library is copyright Chris Toshok. It is released under an X11 license.
 */

package utils.smacker;

import java.awt.Color;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.io.IOException;
import java.io.RandomAccessFile;

import utils.DataReader;

@SuppressWarnings("unused")
public class SmackerDecoder {
	// Palette containts 256 byte triples
	private Color[] currentPalette = new Color[256];
	// File being decoded
	private SmackerFile file;
	private byte[] lastFrameData;
	private byte[][] lastAudioData;
	// Indicates whether the animation is decoded for the first time
	boolean firstTime = true;
	// Palette map used in Smacker
	private byte[] smackerMap = new byte[] { 0x00, 0x04, 0x08, 0x0C, 0x10, 0x14, 0x18, 0x1C, 0x20, 0x24, 0x28, 0x2C, 0x30, 0x34, 0x38, 0x3C, 0x41, 0x45, 0x49, 0x4D, 0x51, 0x55, 0x59, 0x5D, 0x61, 0x65, 0x69, 0x6D, 0x71, 0x75, 0x79, 0x7D, (byte) 0x82, (byte) 0x86, (byte) 0x8A, (byte) 0x8E, (byte) 0x92, (byte) 0x96, (byte) 0x9A, (byte) 0x9E, (byte) 0xA2, (byte) 0xA6, (byte) 0xAA, (byte) 0xAE, (byte) 0xB2, (byte) 0xB6, (byte) 0xBA, (byte) 0xBE, (byte) 0xC3, (byte) 0xC7, (byte) 0xCB, (byte) 0xCF, (byte) 0xD3, (byte) 0xD7, (byte) 0xDB, (byte) 0xDF, (byte) 0xE3, (byte) 0xE7, (byte) 0xEB, (byte) 0xEF, (byte) 0xF3, (byte) 0xF7, (byte) 0xFB, (byte) 0xFF };
	// Runlength map (used in block decoding)
	private int[] sizetable = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 128, 256, 512, 1024, 2048 };
	// Current Frame being decoded
	private int currentFrame;

	/**
	 * Creates a new decoder for the specified file
	 * 
	 * @param file the file to create a decoder for
	 */
	public SmackerDecoder(SmackerFile file) {
		this.file = file;
		this.lastAudioData = new byte[7][];
		this.lastFrameData = new byte[file.getHeader().width * file.getHeader().height];
	}

	private void updatePalette() throws IOException {
		// System.Console.WriteLine("Updating palette");
		RandomAccessFile s = this.file.getStream();
		Color[] OldPallette = this.currentPalette.clone();
		int size = s.read();
		// For some dark reason we need to mask out the lower two bits
		long frameSize = this.file.getFrameSizes()[this.currentFrame] & (~3);
		size = size * 4 - 1;
		frameSize -= size;
		frameSize--;
		int sz = 0;
		long pos = s.getFilePointer() + size;
		int palIndex = 0;
		int j;
		while (sz < 256) {
			int t = s.read();
			if ((t & 0x80) != 0) {
				/* skip palette entries */
				sz += (t & 0x7F) + 1;
				for (int i = 0; i < (t & 0x7F) + 1; i++) {
					this.currentPalette[palIndex++] = new Color(0, 0, 0, 255);
				}
				// palIndex += ((t & 0x7F) + 1) ;
			} else if ((t & 0x40) != 0) {
				/* copy with offset */
				int off = s.read() & 0xFF;
				j = (t & 0x3F) + 1;
				while ((j-- != 0) && sz < 256) {
					this.currentPalette[palIndex++] = OldPallette[off];
					sz++;
					off++;
				}
			} else {
				/* new entries */
				this.currentPalette[palIndex++] = new Color(this.smackerMap[t] & 0xFF, this.smackerMap[(s.read() & 0x3F)] & 0xFF, this.smackerMap[(s.read() & 0x3F)] & 0xFF);
				sz++;
			}
		}
		s.seek(pos);
	}

	public int getIndex(int x, int y) {
		return x + this.file.getHeader().width * y;
	}

	/**
	 * Reads the next frame.
	 * 
	 * @throws NoMoreFramesException
	 * @throws IOException
	 */
	public void readNextFrame() throws NoMoreFramesException, IOException {
		int mask = 1;
		if (this.currentFrame >= this.file.getHeader().nbFrames) {
			throw new NoMoreFramesException();
		}
		long currentPos = this.file.getStream().getFilePointer();
		// If this frame has a palette record
		if ((this.file.getFrameTypes()[this.currentFrame] & mask) > 0) {
			// Update the palette
			updatePalette();
		}
		// Sound data
		mask <<= 1;
		for (int i = 0; i < 7; i++, mask <<= 1) {
			if ((this.file.getFrameTypes()[this.currentFrame] & mask) > 0) {
				long pos = this.file.getStream().getFilePointer();
				int length = DataReader.readInt(this.file.getStream());
				// We assume compression, if not, well too bad
				int unpackedLength = DataReader.readInt(this.file.getStream());
				SmackerBitStream m = new SmackerBitStream(this.file.getStream());
				if (m.readBits(1) != 0) {
					// Audio present
					boolean stereo = m.readBits(1) > 0;
					boolean is16Bit = m.readBits(1) > 0;
					// Next are some trees
					int nbTrees = 1;
					if (stereo) {
						nbTrees <<= 1;
					}
					if (is16Bit) {
						nbTrees <<= 1;
					}
					HuffmanTree[] tree = new HuffmanTree[nbTrees];
					byte[] audioData = new byte[unpackedLength + 4];
					int audioDataIndex = 0;
					for (int k = 0; k < nbTrees; k++) {
						tree[k] = new HuffmanTree();
						tree[k].buildTree(m);
					}
					int res;
					if (is16Bit) {
						short rightBaseMSB = 0, rightBaseLSB = 0, leftBaseMSB = 0, leftBaseLSB = 0;
						rightBaseMSB = (short) (m.readBits(8));
						rightBaseLSB = (short) (m.readBits(8));
						// Add sample (little endian)
						// Lower byte
						audioData[audioDataIndex++] = (byte) rightBaseLSB;
						// Higher byte
						audioData[audioDataIndex++] = (byte) rightBaseMSB;
						if (stereo) {
							leftBaseMSB = (short) (m.readBits(8));
							leftBaseLSB = (short) (m.readBits(8));
							// Add sample (little endian)
							// Lower byte
							audioData[audioDataIndex++] = (byte) leftBaseLSB;
							// Higher byte
							audioData[audioDataIndex++] = (byte) leftBaseMSB;
						}
						for (int l = 0; l < unpackedLength / 2; l++) {
							if ((l & ((stereo) ? 1 : 0)) > 0) {
								res = tree[2].decode(m);
								leftBaseLSB += (short) res;
								res = tree[3].decode(m);
								leftBaseMSB += (short) res;
								leftBaseMSB += (short) (leftBaseLSB >> 8);
								leftBaseLSB &= 0xFF;
								// Add sample (little endian)
								// Lower byte
								audioData[audioDataIndex++] = (byte) leftBaseLSB;
								// Higher byte
								audioData[audioDataIndex++] = (byte) leftBaseMSB;
							} else {
								res = tree[0].decode(m);
								rightBaseLSB += (short) res;
								res = tree[1].decode(m);
								rightBaseMSB += (short) res;
								rightBaseMSB += (short) (rightBaseLSB >> 8);
								rightBaseLSB &= 0xFF;
								// Add sample (little endian)
								// Lower byte
								audioData[audioDataIndex++] = (byte) rightBaseLSB;
								// Higher byte
								audioData[audioDataIndex++] = (byte) rightBaseMSB;
							}
						}
					} else {
						byte rightBase = (byte) m.readBits(8), leftBase = 0;
						// Add sample
						audioData[audioDataIndex++] = rightBase;
						if (stereo) {
							leftBase = (byte) m.readBits(8);
							// Add sample
							audioData[audioDataIndex++] = leftBase;
						}
						for (int l = 0; l < unpackedLength; l++) {
							if ((l & ((stereo) ? 1 : 0)) > 0) {
								leftBase += (byte) tree[1].decode(m);
								// Add sample
								audioData[audioDataIndex++] = leftBase;
							} else {
								rightBase += (byte) tree[0].decode(m);
								// Add sample
								audioData[audioDataIndex++] = rightBase;
							}
						}
					}
					this.lastAudioData[i] = audioData;
				}
				this.file.getStream().seek(pos + length);
			}
		}
		// Video data
		decodeVideo();
		// Seek to the next frame
		this.file.getStream().seek(currentPos + this.file.getFrameSizes()[this.currentFrame]);
		this.currentFrame++;
	}

	/**
	 * Returns the audiodata from the specified audiostream
	 * 
	 * @param streamIndex The index of the stream to return audio data for,
	 *        should be between 0 and 7
	 * @return PCM Audio data in a byte array
	 */
	public byte[] getAudioData(int streamIndex) {
		return this.lastAudioData[streamIndex];
	}

	private void decodeVideo() throws IOException {
		int x, y, mask, currentBlock = 0, runLength, colors, blockHeader, blockType = 0;
		int posX, posY, index, pix, pix1, pix2, i, j;
		byte color, color1, color2;
		// Reset all huffman decoders
		this.file.getmClr().resetDecoder();
		this.file.getmMap().resetDecoder();
		this.file.getType().resetDecoder();
		this.file.getFull().resetDecoder();
		// Allocate a new frame's data
		byte[] currentFrameData = new byte[this.file.getHeader().width * this.file.getHeader().height];
		SmackerBitStream m = new SmackerBitStream(this.file.getStream());
		int nbBlocksX = this.file.getHeader().width / 4;
		int nbBlocksY = this.file.getHeader().height / 4;
		int nbBlocks = nbBlocksX * nbBlocksY;
		long runLengthNotComplete = 0;
		while (currentBlock < nbBlocks) {
			blockHeader = this.file.getType().decode(m);
			runLength = this.sizetable[(blockHeader >> 2) & 0x3F];
			blockType = blockHeader & 3;
			// System.Console.Write("BLOCK " + currentBlock + " " + runLength);
			switch (blockType) {
			case 2:
				// VOID BLOCK
				// System.Console.WriteLine("VOID - ");
				// Get block address
				for (i = 0; i < runLength && currentBlock < nbBlocks; i++) {
					// Get current block coordinates
					posX = 4 * (currentBlock % nbBlocksX);
					posY = 4 * (currentBlock / nbBlocksX);
					index = 0;
					for (x = 0; x < 4; x++) {
						for (y = 0; y < 4; y++) {
							index = getIndex(posX + x, posY + y);
							currentFrameData[index] = this.lastFrameData[index];
						}
					}
					currentBlock++;
				}
				runLengthNotComplete = runLength - i;
				break;
			case 3:
				// SOLID BLOCK
				// System.Console.WriteLine("SOLID - ");
				color = (byte) (blockHeader >> 8);
				// Get block address
				for (i = 0; i < runLength && currentBlock < nbBlocks; i++) {
					// Get current block coordinates
					posX = 4 * (currentBlock % nbBlocksX);
					posY = 4 * (currentBlock / nbBlocksX);
					for (x = 0; x < 4; x++) {
						for (y = 0; y < 4; y++) {
							currentFrameData[getIndex(posX + x, posY + y)] = color;
						}
					}
					currentBlock++;
				}
				runLengthNotComplete = runLength - i;
				break;
			case 0:
				// MONO BLOCK
				// System.Console.WriteLine("MONO - ");
				for (i = 0; i < runLength && currentBlock < nbBlocks; i++) {
					colors = this.file.getmClr().decode(m);
					color1 = (byte) (colors >> 8);
					color2 = (byte) (colors & 0xFF);
					mask = this.file.getmMap().decode(m);
					posX = (currentBlock % nbBlocksX) * 4;
					posY = (currentBlock / nbBlocksX) * 4;
					for (y = 0; y < 4; y++) {
						if ((mask & 1) > 0) {
							currentFrameData[getIndex(posX, posY + y)] = color1;
						} else {
							currentFrameData[getIndex(posX, posY + y)] = color2;
						}
						if ((mask & 2) > 0) {
							currentFrameData[getIndex(posX + 1, posY + y)] = color1;
						} else {
							currentFrameData[getIndex(posX + 1, posY + y)] = color2;
						}
						if ((mask & 4) > 0) {
							currentFrameData[getIndex(posX + 2, posY + y)] = color1;
						} else {
							currentFrameData[getIndex(posX + 2, posY + y)] = color2;
						}
						if ((mask & 8) > 0) {
							currentFrameData[getIndex(posX + 3, posY + y)] = color1;
						} else {
							currentFrameData[getIndex(posX + 3, posY + y)] = color2;
						}
						mask >>= 4;
					}
					currentBlock++;
				}
				// runLengthNotComplete = runLength - i;
				break;
			case 1:
				// System.Console.WriteLine("FULL - ");
				int mode = 0;
				if (this.file.isV4()) {
					int type = m.readBits(1);
					if (type == 0) {
						int abit = m.readBits(1);
						if (abit == 1) {
							mode = 2;
						}
					} else {
						mode = 1;
					}
				}
				switch (mode) {
				case 0:
					// v2 Full block
					for (i = 0; i < runLength && currentBlock < nbBlocks; i++) {
						posX = (currentBlock % nbBlocksX) * 4;
						posY = (currentBlock / nbBlocksX) * 4;
						for (y = 0; y < 4; y++) {
							colors = this.file.getFull().decode(m);
							color1 = (byte) (colors >> 8);
							color2 = (byte) (colors & 0xFF);
							currentFrameData[getIndex(posX + 3, posY + y)] = color1;
							currentFrameData[getIndex(posX + 2, posY + y)] = color2;
							colors = this.file.getFull().decode(m);
							color1 = (byte) (colors >> 8);
							color2 = (byte) (colors & 0xFF);
							currentFrameData[getIndex(posX + 1, posY + y)] = color1;
							currentFrameData[getIndex(posX + 0, posY + y)] = color2;
						}
						currentBlock++;
					}
					break;
				case 1:
					for (i = 0; i < runLength && currentBlock < nbBlocks; i++) {
						posX = (currentBlock % nbBlocksX) * 4;
						posY = (currentBlock / nbBlocksX) * 4;
						pix = this.file.getFull().decode(m);
						color1 = (byte) (pix >> 8);
						color2 = (byte) (pix & 0xFF);
						currentFrameData[getIndex(posX + 0, posY + 0)] = color2;
						currentFrameData[getIndex(posX + 1, posY + 0)] = color2;
						currentFrameData[getIndex(posX + 2, posY + 0)] = color1;
						currentFrameData[getIndex(posX + 3, posY + 0)] = color1;
						currentFrameData[getIndex(posX + 0, posY + 1)] = color2;
						currentFrameData[getIndex(posX + 1, posY + 1)] = color2;
						currentFrameData[getIndex(posX + 2, posY + 1)] = color1;
						currentFrameData[getIndex(posX + 3, posY + 1)] = color1;
						pix = this.file.getFull().decode(m);
						color1 = (byte) (pix >> 8);
						color2 = (byte) (pix & 0xFF);
						currentFrameData[getIndex(posX + 0, posY + 2)] = color2;
						currentFrameData[getIndex(posX + 1, posY + 2)] = color2;
						currentFrameData[getIndex(posX + 2, posY + 2)] = color1;
						currentFrameData[getIndex(posX + 3, posY + 2)] = color1;
						currentFrameData[getIndex(posX + 0, posY + 3)] = color2;
						currentFrameData[getIndex(posX + 1, posY + 3)] = color2;
						currentFrameData[getIndex(posX + 2, posY + 3)] = color1;
						currentFrameData[getIndex(posX + 3, posY + 3)] = color1;
						currentBlock++;
					}
					// runLengthNotComplete = runLength - i;
					break;
				case 2:
					for (j = 0; j < runLength && currentBlock < nbBlocks; j++) {
						posX = (currentBlock % nbBlocksX) << 2;
						posY = (currentBlock / nbBlocksX) << 2;
						for (i = 0; i < 2; i++) {
							pix1 = this.file.getFull().decode(m);
							pix2 = this.file.getFull().decode(m);
							color1 = (byte) (pix1 >> 8);
							color2 = (byte) (pix1 & 0xFF);
							currentFrameData[getIndex(posX + 2, posY + (i << 1))] = color2;
							currentFrameData[getIndex(posX + 3, posY + (i << 1))] = color1;
							currentFrameData[getIndex(posX + 2, posY + (i << 1) + 1)] = color2;
							currentFrameData[getIndex(posX + 3, posY + (i << 1) + 1)] = color1;
							color1 = (byte) (pix1 >> 8);
							color2 = (byte) (pix1 & 0xFF);
							currentFrameData[getIndex(posX + 0, posY + (i << 1))] = color2;
							currentFrameData[getIndex(posX + 1, posY + (i << 1))] = color1;
							currentFrameData[getIndex(posX + 0, posY + (i << 1) + 1)] = color2;
							currentFrameData[getIndex(posX + 1, posY + (i << 1) + 1)] = color1;
						}
						currentBlock++;
					}
					// runLengthNotComplete = runLength - j;
					break;
				default:
					break;
				}
				break;
			}
		}
		// if (runLengthNotComplete > 0) {
		// Console.WriteLine("Warning: frame ended before runlength has reached zero");
		// }
		this.lastFrameData = currentFrameData;
	}

	/**
	 * Encapsulates the video data from the last decoded frame in a
	 * BufferedImage
	 * 
	 * @return A BufferedImage
	 */
	public BufferedImage getVideoDataBitmap() {
		int width = this.file.getHeader().width;
		int height = this.file.getHeader().height;
		byte[] iArray = new byte[width * height * 3];
		int j = 0;
		for (byte i : this.lastFrameData) {
			iArray[j] = (byte) this.currentPalette[i & 0xFF].getRed();
			iArray[j + 1] = (byte) this.currentPalette[i & 0xFF].getGreen();
			iArray[j + 2] = (byte) this.currentPalette[i & 0xFF].getBlue();
			j += 3;
		}
		DataBufferByte dbb = new DataBufferByte(iArray, iArray.length);
		ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8, 8 }, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
		return new BufferedImage(cm, Raster.createInterleavedRaster(dbb, width, height, width * 3, 3, new int[] { 0, 1, 2 }, null), false, null);
	}

	/**
	 * Resets the decoder to the first frame, if there is a ring frame the first
	 * frame is skipped as it should.
	 */
	public void reset() throws IOException {
		int nbFrames = this.file.getHeader().nbFrames;
		if (this.file.getHeader().hasRingFrame()) {
			nbFrames++;
		}
		// Seek to the beginning of the frame data section
		// Header = 104 bytes, 5 bytes per frame (one dword + one byte) + trees
		int pos = 104 + 5 * nbFrames + this.file.getHeader().treesSize;
		this.file.getStream().seek(pos);
		this.currentFrame = 0;
		// The ring frame replace the first frame on the second+ run.
		if (!this.firstTime && this.file.getHeader().hasRingFrame()) {
			// Seek ahead 1 frame
			this.file.getStream().skipBytes(this.file.getFrameSizes()[0]);
			this.currentFrame = 1;
		}
		this.firstTime = false;
	}

	/**
	 * Set the decoder to decode the specified frame next
	 * 
	 * @param i the index of the next frame the decoder should decode
	 */
	public void seekTo(int i) throws IOException {
		// Seek to frame 0
		reset();
		if (i >= this.file.getHeader().nbFrames) {
			throw new IndexOutOfBoundsException("Not a valid frame number!");
		}
		int total = 0;
		for (int j = 0; j < i; j++) {
			total += this.file.getFrameSizes()[j];
		}
		this.currentFrame += i - 1;
		this.file.getStream().skipBytes(total);
	}

	public SmackerFile getFile() {
		return this.file;
	}
}
